##########################################
## Copyright 2012-2013 Ceruti Francesco & contributors
##
## This file is part of LiSP (Linux Show Player).
##########################################

import gi
gi.require_version('Gst', '1.0')

from math import pow
from time import sleep
from threading import Thread
from multiprocessing import cpu_count

from gi.repository import Gst
from PyQt4 import QtCore, QtGui

from lisp.core.media import Media
from lisp.core.actions import Action
from lisp.core.plugin import Plugin
from lisp.utils.progress import Progress


class GainAction(Action):

    hasInfo = True

    def __init__(self):
        self.__mediaList = []
        self.__newVolumes = []
        self.__oldVolumes = []

    def addMedia(self, media, volume):
        self.__mediaList.append(media)
        self.__newVolumes.append(volume)
        self.__oldVolumes.append(media.conf['volume']['volume'])

    def do(self):
        for n in range(len(self.__mediaList)):
            self.__mediaList[n].updateConfiguration({'volume': {'volume': self.__newVolumes[n], 'mute': self.__mediaList[n].conf['volume']['mute']}})

    def undo(self):
        for n in range(len(self.__mediaList)):
            self.__mediaList[n].updateConfiguration({'volume': {'volume': self.__oldVolumes[n], 'mute': self.__mediaList[n].conf['volume']['mute']}})

    def redo(self):
        self.do()

    def info(self):
        return 'Replay gain volume adjust'


class ReplayGain(QtCore.QObject, Plugin):

    PLUGIN_NAME = 'ReplayGain'

    MAX_GAIN = 20  # dB

    def __init__(self, main):
        super(ReplayGain, self).__init__()
        self.mainWindow = main.mainWindow
        self.collector = main.collector

        self.gained = []
        self.__countdown = 0
        self.threads = 1

        # Voice in mainWindow menu under 'tools'
        self.actionGain = QtGui.QAction(self.mainWindow, triggered=self.gain)
        self.actionGain.setText("ReplayGain / Normalization")
        self.mainWindow.menuTools.addAction(self.actionGain)

        self.progress = Progress(title='ReplayGain / Normalization')

    def gain(self):
        gainUi = GainUi(self.mainWindow)
        gainUi.exec_()

        if(gainUi.result() == QtGui.QDialog.Accepted):
            self.gainSelctedMode = gainUi.checkBox.isChecked()

            if(self.gainSelctedMode):
                mediaList = self.mainWindow.collectorUi.mediaSelectedList()
            else:
                mediaList = self.mainWindow.collectorUi.mediaList()

            for media in mediaList:
                if(media.state != Media.NONE):
                    self.gained.append(media)

            self.__countdown = len(self.gained)
            self.progress.reset()
            self.progress.setMaximum(self.__countdown * 10)

            self.gainMode = gainUi.gainMode
            self.normLevel = gainUi.normLevel
            self.refLevel = gainUi.ref_level
            self.threads = gainUi.threads
            self.calcGain()

    def calcGain(self):
        self.progress.setLabelText('Analysis ...')
        self.progress.show()

        self.__thread = Thread(target=self.executor)
        self.__thread.setDaemon(True)
        self.__thread.start()

    def executor(self):
        self.__media_counter = 0
        self.__thread_counter = 0

        self.__action = GainAction()

        while(self.__media_counter < len(self.gained)):
            if(self.__thread_counter < self.threads):
                gain = GainCore()
                gain.onGain.connect(self.__free_thread)
                gain.onGain.connect(self.applyGain, QtCore.Qt.QueuedConnection)

                thread = Thread(target=gain.gain, args=(self.gained[self.__media_counter], self.refLevel))
                thread.setDaemon(True)
                thread.start()

                self.__media_counter += 1
                self.__thread_counter += 1
            else:
                sleep(0.1)

    def __free_thread(self):
            self.__thread_counter -= 1

    def applyGain(self, gained, gain, peak, media):
        self.__countdown -= 1

        if(gained):
            if(gain > ReplayGain.MAX_GAIN):
                gain = ReplayGain.MAX_GAIN

            if(self.gainMode == 0):
                volume = min(1 / peak, pow(10, (gain) / 20))
            elif(self.gainMode == 1):
                volume = 1 / peak * pow(10, (self.normLevel) / 20)

            self.__action.addMedia(media, volume)

            self.progress.setValue(self.progress.value() + 10)

        if(self.__countdown == 0):
            self.__action.exec_()
            self.progress.hide()


class GainCore(QtCore.QObject):

    onGain = QtCore.pyqtSignal(bool, float, float, object)

    def __init__(self):
        super(GainCore, self).__init__()
        self.media = None
        self.current_state = None

    def on_message(self, bus=None, message=None):
        mType = message.type
        if(mType == Gst.MessageType.TAG):
            tags = message.parse_tag()
            tag = tags.get_double(Gst.TAG_TRACK_GAIN)
            peak = tags.get_double(Gst.TAG_TRACK_PEAK)
            if(tag[0] == True and peak[0] == True):
                self.gain_pipe.set_state(Gst.State.NULL)
                self.onGain.emit(True, tag[1], peak[1], self.media)
        elif(mType == Gst.MessageType.ERROR):
            err, debug = message.parse_error()
            self.onGain.emit(False, 0, 0, self.media)
            raise Exception('Error: ' + str(err) + '\n', str(debug))
        elif(mType == Gst.MessageType.STATE_CHANGED):
            self.current_state = message.parse_state_changed()[1].value_name

    # Create a pipeline with a fake audio output and get, via bus, the gain levels
    def gain(self, media, ref_level):
        self.media = media
        self.gain_pipe = Gst.parse_launch('filesrc location="' + media.conf['location'] + '" ! decodebin ! audioconvert ! rganalysis reference-level=' + str(ref_level) + ' ! fakesink')
        gain_bus = self.gain_pipe.get_bus()
        gain_bus.add_signal_watch()
        gain_bus.connect("message", self.on_message)
        self.gain_pipe.set_state(Gst.State.PLAYING)


class GainUi(QtGui.QDialog):

    def __init__(self, parent=None):
        super(GainUi, self).__init__(parent)

        self.setWindowModality(QtCore.Qt.ApplicationModal)
        self.setMaximumSize(380, 210)
        self.setMinimumSize(380, 210)
        self.resize(380, 210)

        self.verticalLayoutWidget = QtGui.QWidget(self)
        self.verticalLayoutWidget.setGeometry(QtCore.QRect(10, 5, 360, 210))
        self.verticalLayout = QtGui.QVBoxLayout(self.verticalLayoutWidget)

        self.horizontalLayout_2 = QtGui.QHBoxLayout()

        self.radioButton = QtGui.QRadioButton(self.verticalLayoutWidget)
        self.radioButton.setChecked(True)
        self.horizontalLayout_2.addWidget(self.radioButton)

        self.spinBox = QtGui.QSpinBox(self.verticalLayoutWidget)
        self.spinBox.setMaximum(150)
        self.spinBox.setProperty("value", 89)
        self.horizontalLayout_2.addWidget(self.spinBox)

        self.verticalLayout.addLayout(self.horizontalLayout_2)

        self.horizontalLayout = QtGui.QHBoxLayout()

        self.radioButton_2 = QtGui.QRadioButton(self.verticalLayoutWidget)
        self.horizontalLayout.addWidget(self.radioButton_2)

        self.spinBox_2 = QtGui.QSpinBox(self.verticalLayoutWidget)
        self.spinBox_2.setMinimum(-100)
        self.spinBox_2.setMaximum(0)
        self.spinBox_2.setEnabled(False)
        self.horizontalLayout.addWidget(self.spinBox_2)

        self.verticalLayout.addLayout(self.horizontalLayout)

        self.checkBox = QtGui.QCheckBox(self.verticalLayoutWidget)
        self.verticalLayout.addWidget(self.checkBox)

        self.line = QtGui.QFrame(self.verticalLayoutWidget)
        self.line.setFrameShape(QtGui.QFrame.HLine)
        self.line.setFrameShadow(QtGui.QFrame.Sunken)
        self.verticalLayout.addWidget(self.line)

        self.horizontalLayout_4 = QtGui.QHBoxLayout()

        self.label_3 = QtGui.QLabel(self.verticalLayoutWidget)
        self.horizontalLayout_4.addWidget(self.label_3)

        self.spinBox_3 = QtGui.QSpinBox(self.verticalLayoutWidget)
        self.spinBox_3.setMinimum(1)
        self.spinBox_3.setMaximum(64)
        self.spinBox_3.setValue(cpu_count())
        self.horizontalLayout_4.addWidget(self.spinBox_3)

        self.verticalLayout.addLayout(self.horizontalLayout_4)

        self.horizontalLayout_3 = QtGui.QHBoxLayout()

        self.pushButton_3 = QtGui.QPushButton(self.verticalLayoutWidget)
        self.horizontalLayout_3.addWidget(self.pushButton_3)

        self.pushButton_2 = QtGui.QPushButton(self.verticalLayoutWidget)
        self.horizontalLayout_3.addWidget(self.pushButton_2)

        self.verticalLayout.addLayout(self.horizontalLayout_3)

        self.buttonGroup = QtGui.QButtonGroup()
        self.buttonGroup.addButton(self.radioButton)
        self.buttonGroup.addButton(self.radioButton_2)

        self.radioButton_2.toggled.connect(self.spinBox_2.setEnabled)
        self.radioButton_2.toggled.connect(self.spinBox.setDisabled)

        self.pushButton_3.clicked.connect(self.reject)
        self.pushButton_2.clicked.connect(self.accept)

        self.retranslateUi()

    def retranslateUi(self):
        self.setWindowTitle("Gain")
        self.label_3.setText("Threads number")
        self.checkBox.setText("Apply only to selected media")
        self.radioButton.setText("ReplayGain to (dB SPL)")
        self.radioButton_2.setText("Normalize to (dB)")
        self.pushButton_2.setText("Apply")
        self.pushButton_3.setText("Cancel")

    def accept(self):
        self.gainMode = -(self.buttonGroup.checkedId() + 2)
        self.normLevel = self.spinBox_2.value()
        self.ref_level = self.spinBox.value()
        self.threads = self.spinBox_3.value()
        super().accept()
